package pl.edu.agh.jemo.evolution.algorithm.impl;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import javax.swing.JPanel;

import org.apache.log4j.Logger;

import pl.edu.agh.jemo.conf.types.PrintableType;
import pl.edu.agh.jemo.evolution.algorithm.AbstractAlgorithm;
import pl.edu.agh.jemo.evolution.operator.mutation.impl.Hillclimbing;
import pl.edu.agh.jemo.evolution.population.Population;
import pl.edu.agh.jemo.evolution.selections.impl.SPEA2EnvironmentalSelection;
import pl.edu.agh.jemo.evolution.specimen.Domination;
import pl.edu.agh.jemo.evolution.specimen.Specimen;
import pl.edu.agh.jemo.evolution.specimen.impl.SPEA2Specimen;
import pl.edu.agh.jemo.gui.LoggerHelper;
import pl.edu.agh.jemo.gui.chart.Plot;
import pl.edu.agh.jemo.gui.panels.algorithm.SPEA2ConfigurationPanel;

/**
 * Implementation of SPEA2 algorithm based on AbstractAlgorithm class functionallity.
 * In this implementation only allowed Specimen type is SPEA2Specimen. This is required by special selection type used by SPEA2 algorithm.
 * 
 * @author Marcin Tkacz
 *
 */
public class SPEA2Algorithm extends AbstractAlgorithm {

	Logger logger = LoggerHelper.getInstance().getLogger();
	/**
	 * Size of result population named archive in SPEA2 algorithm. Should be less then population size.
	 */
	protected int archiveSize = 30;
	/**
	 * Result population containing best specimen found, named archive.
	 */
	protected Population archive = new Population();
	/**
	 * Multiplier used by hillclimbing mutation which is always used by SPEA in addition to normal mutation.
	 */
	protected double hillClimbingMultiplier = 1.;

	/**
	 * Special selection used only by SPEA algorithm.
	 */
	protected SPEA2EnvironmentalSelection spea2Selection = new SPEA2EnvironmentalSelection();
	/**
	 * Hillclimbing mutation always used by SPEA algorithm in addition to normal mutation.
	 */
	protected Hillclimbing hillClimbing = new Hillclimbing();
	
	/**
	 * Calculates how close is given specimen, to rest of the population.
	 * Calculations are based on genotype space distance between given specimen, and it's kth nearest neighbour.
	 * Where k stands for square root of number of specimen. Result is stored as density factor parameter of given specimen.
	 */
	protected void calculateDensityFactors() {
		Population sumPopulation = new Population();
		sumPopulation.addAll(population);
		sumPopulation.addAll(archive);

		int kthNearestNeighbour = Math.round((float) Math.sqrt(sumPopulation.size()));

		final List<Double> distances = new ArrayList<Double>();
		for (int i = 0; i < sumPopulation.size(); i++) {
			distances.clear();
			for (int k = 0; k < sumPopulation.size(); k++) {
				final double distance = sumPopulation.get(i)
						.getEuclideanObjectiveSpaceDistance(sumPopulation.get(k));
				distances.add(distance);
			}
			Collections.sort(distances);
			distances.remove(0);
			final Double densityFactor = 1.0d / (distances
					.get(kthNearestNeighbour) + 2);
			((SPEA2Specimen) sumPopulation.get(i)).setDensityFactor(densityFactor);
		}
	}

	/**
	 * Computes strength for given specimen instance.
	 * Strength is a number of specimen dominated by the one given in parameter.
	 * 
	 * @param specimen specimen for which strength should be calculated.
	 */
	private void calculateStrengthFor(SPEA2Specimen specimen) {
		int dominates = 0;
		final Population union = new Population();
		union.addAll(population);
		union.addAll(archive);

		for (Specimen spec : union) {
			final Domination relation = specimen.dominationRelationWith(spec);
			if (relation == Domination.DOMINATES) {
				dominates++;
			}
		}
		specimen.setStrength((double)dominates);
	}

	/**
	 * Computes fitness for given specimen.
	 * Fitness is a sum of strengths of all dominated specimen, and specimens density factor.
	 * 
	 * @param specimen for which fitness is calculated.
	 */
	private void calculateFitnessFor(SPEA2Specimen specimen) {
		Double fitness = 0.;
		final Population union = new Population();
		union.addAll(population);
		union.addAll(archive);

		for (Specimen spec : union) {
			final Domination relation = specimen.dominationRelationWith(spec);
			if (relation == Domination.IS_DOMINATED) {
				fitness += ((SPEA2Specimen) spec).getStrength();
			}
		}
		specimen.setFitness(fitness + specimen.getDensityFactor());
	}

	/**
	 * Calculates fitness for all specimen both in population and archive.
	 */
	private void calculateFitness() {
		for (Specimen specimen : population) {
			calculateFitnessFor((SPEA2Specimen) specimen);
		}
		for (Specimen specimen : archive) {
			calculateFitnessFor((SPEA2Specimen) specimen);
		}
	}

	/**
	 * Calculates strength of all specimen in population and archive.
	 */
	private void calculateStrength() {
		for (Specimen specimen : population) {
			calculateStrengthFor((SPEA2Specimen) specimen);
		}
		for (Specimen specimen : archive) {
			calculateStrengthFor((SPEA2Specimen) specimen);
		}
	}

	/**
	 * Performs SPEA2 specific initialization.
	 * Prepares hillclimbing mutation and SPEA2 selection, basing on parameters passed to algorithm.
	 * This method initializes also archive population. 
	 */
	@Override
	protected void performInit() {
		
		spea2Selection.setArchiveSize(archiveSize);
		hillClimbing.setMultiplier(hillClimbingMultiplier);

		try {
			archive = population.clone();
			archive.clear();
		} catch (Exception e) {
			throw new RuntimeException(e);
		}
		logger.debug("archive initialized");
	}

	/**
	 * Single step of SPEA2 algorithm.
	 * Generates next generation of specimen, by populating an archive and performing genetic operators both on sum of archive and population.
	 * Result is stored as a child population for next step.
	 */
	@Override
	protected void step() {

		calculatePhenotype();
		calculateStrength();
		logger.debug("strength");
		calculateDensityFactors();
		logger.debug("density factors");
		calculateFitness();
		logger.debug("fitness");
		archive = spea2Selection.generateNewArchive(population, archive);
		logger.debug("archive");
		
		final Population matingPool = archive.clone();
		tournament.performTournament(matingPool);
		logger.debug("tournament performed");
		matingPool.recombinate(crossover);
		logger.debug("crossover performed");		
		matingPool.mutate(mutation);
		logger.debug("mutation performed");
		matingPool.mutate(hillClimbing);
		logger.debug("hillclimbing performed");
		population = matingPool;	
	}

	/**
	 * Returns size of archive population used by algorithm.
	 * 
	 * @return Expected nnumber of specimen in archive population.
	 */
	public int getArchiveSize() {
		return archiveSize;
	}

	/**
	 * Sets expected archive size.
	 * 
	 * @param archiveSize Number of specimen to be used as an archive.
	 */
	public void setArchiveSize(int archiveSize) {
		this.archiveSize = archiveSize;
	}

	/**
	 * Method called at the end of algorithm.
	 * In this case, this method is empty, as SPEA2 algorithm requires no additional computations after algorithm run.
	 */
	@Override
	protected void endAlgorithm() {
	}
	
	/**
	 * Plots both archive and population.
	 * This method is extension of AbstractAlgorithm method called after every step of algorithm. It is added, as SPEA2 introduces archive which is as important as normal population, from result analysis point of view.
	 */
	@Override
	protected void plotPopulation() {
		super.plotPopulation();
		Plot plot = new Plot();
		plot.setName("Archive");
		plot.addPopulation(archive);
		plotter.addPlot(plot);		
	}
	
	/**
	 * Filters specimen types list and leaves these, that can be used with SPEA2 algorithm.
	 * This algorithm works only with SPEA2Specimen and it's subclasses, so method accepts only these.
	 *  
	 * @param specimenTypesList Collection of all specimen to be validated. Specimen types should be wrapped in PrintableType.
	 * @return List of specimen types extending SPEA2Specimen type, wrapped in PrintableType instances.
	 */
	@Override
	public List<PrintableType<? extends Specimen>> validateSpecimenTypes(List<PrintableType<? extends Specimen>> specimenTypesList) {
		List<PrintableType<? extends Specimen>> returnList = new ArrayList<PrintableType<? extends Specimen>>();
		for (PrintableType<? extends Specimen> type : specimenTypesList) {
			if (SPEA2Specimen.class.isAssignableFrom(type.getClassType())) {
				returnList.add(type);
			}
		}
		return returnList;
	}

	/**
	 * Returns configuration panel being extension of JPanel instance.
	 * Returned panel is displayed by graphical user interface allowing user to confgiure algorithm.
	 * This method may return null if no configuration is possible.
	 * 
	 * @return Instance of configuration panel extending JPanel, and initialized to configure this instance of algorithm.
	 */
	@Override
	public JPanel getConfigPanel() {
		return new SPEA2ConfigurationPanel(this);
	}
	
	/**
	 * Returns currently set hillclimbing mutation multiplier.
	 * 
	 * @return Value of parameter multiplier, for built in hillclimbing mutation.
	 */
	public double getHillClimbingMultiplier() {
		return hillClimbingMultiplier;
	}

	/**
	 * Sets multiplier for built in hillclimbing mutation.
	 * You may turn this mutation off, by passing here 0.
	 * 
	 * @param hillClimbingMultiplier Value of multiplier to be used by algorithm.
	 */
	public void setHillClimbingMultiplier(double hillClimbingMultiplier) {
		this.hillClimbingMultiplier = hillClimbingMultiplier;
	}
	
	/**
	 * Returns String object containing description of spea2 algorithm configuration in readable form as a summary.
	 * 
	 * @return String description of spea2 algorithm configuration.
	 */	
	@Override
	public String toString() {
		StringBuilder sb = new StringBuilder();
		sb.append(super.toString());
		sb.append("Archive size: " + archiveSize + "\n");
		sb.append("Hillclimbing multiplier: " + hillClimbingMultiplier + "\n");
		return sb.toString();
	}
}
